﻿using Microsoft.Diagnostics.Tracing;
using Microsoft.Diagnostics.Tracing.Parsers;
using Microsoft.Diagnostics.Tracing.Session;
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Diagnostics;
using System.Linq;
using System.Management;
using System.Text;
using System.Threading.Tasks;

namespace Reflexive.IO
{
    public class ETWReader<TConverter, TDestination> : IDisposable, IReflexive<TConverter, TraceEvent, TDestination>
        where TConverter : ITypeConverter<TraceEvent, TDestination>, new()
        where TDestination : class
    {
        TraceEventSession _session;
        ETWTraceEventSource _source;
        string _sessionName = Guid.NewGuid().ToString("d");
        SynchronizedCollection<Subscription<TDestination>> _observers = new SynchronizedCollection<Subscription<TDestination>>();

        public StatusCode Status
        {
            get;
            private set;
        }

        public ETWReader(string providerName)
        {
            // Given just the name of the eventSource you can get the GUID for the evenSource by calling this API.  
            // From a ETW perspective, the GUID is the 'true name' of the EventSource.  
            var providerGuid = TraceEventProviders.GetEventSourceGuidFromName(providerName);

            // Today you have to be Admin to turn on ETW events (anyone can write ETW events).   
            if (!(TraceEventSession.IsElevated() ?? false))
            {
                throw new UnauthorizedAccessException("To turn on ETW events you need to be Administrator, please run from an Admin process.");
            }

            // As mentioned below, sessions can outlive the process that created them.  Thus you need a way of 
            // naming the session so that you can 'reconnect' to it from another process.   This is what the name
            // is for.  It can be anything, but it should be descriptive and unique.   If you expect mulitple versions
            // of your program to run simultaneously, you need to generate unique names (e.g. add a process ID suffix) 
            _session = new TraceEventSession(_sessionName, null);  // the null second parameter means 'real time session'

            // Note that sessions create a OS object (a session) that lives beyond the lifetime of the process
            // that created it (like Filles), thus you have to be more careful about always cleaning them up. 
            // An importanty way you can do this is to set the 'StopOnDispose' property which will cause the session to 
            // stop (and thus the OS object will die) when the TraceEventSession dies.   Because we used a 'using'
            // statement, this means that any exception in the code below will clean up the OS object.   
            _session.StopOnDispose = true;

            // prepare to read from the session, connect the ETWTraceEventSource to the session
            _source = new ETWTraceEventSource(_sessionName, TraceEventSourceType.Session);
            // To demonstrate non-trivial event manipuation, we calculate the time delta between 'MyFirstEvent and 'MySecondEvent'
            // These variables are used in this calculation 
            // Hook up the parser that knows about EventSources
            var parser = new DynamicTraceEventParser(_source);
            parser.All += delegate(TraceEvent message)
            {
                try
                {
                    _observers.OnNext<TDestination>((Convertible<TConverter, TraceEvent, TDestination>)message);
                }
                catch (Exception e)
                {
                    System.Diagnostics.Trace.TraceError(e.ToString()); 
                }

            };

            // Enable my provider, you can call many of these on the same session to get other events.  
            _session.EnableProvider(providerGuid);
        }

        public ETWReader(string providerName, string sessionName):this(providerName)
        {
            _sessionName = sessionName;
        }

        public void Start()
        {
            if (Status == StatusCode.Completed)
            {
                throw new NotSupportedException("Reader is in the Completed state.");
            }

            // go into a loop processing events can calling the callbacks.  Because this is live data (not from a file)
            // processing never completes by itself, but only because someone called 'source.Close()'.  
            Task.Run(() =>
            {
                try
                {
                    _source.Process();
                }
                catch (Exception e)
                {
                    System.Diagnostics.Trace.TraceError(e.ToString());
                    try
                    {
                        _observers.OnError<TDestination>(e);
                    }
                    catch (Exception ex)
                    {
                        System.Diagnostics.Trace.TraceError(ex.ToString());
                    }
                }
            }).ConfigureAwait(continueOnCapturedContext: true);
            Status = StatusCode.Started;
        }

        public void Stop()
        {
            try
            {
                _source.StopProcessing();
            }
            catch
            {
                throw;
            }
            finally
            {
                Status = StatusCode.Suspended;
            }
        }

        #region Subscription Management

        public IDisposable Subscribe(IObserver<TDestination> observer)
        {
            var subscription = new Subscription<TDestination>(observer);
            subscription.UnSubscribe += UnSubscribe;
            _observers.Add(subscription);
            return subscription;
        }

        private void UnSubscribe(object sender, IObserver<TDestination> e)
        {
            _observers.Remove(((Subscription<TDestination>)sender));
        }

        #endregion

        public void Dispose()
        {
            if (Status != StatusCode.Completed)
            {
                try
                {
                     Stop();
                    _source.Dispose();
                    _session.Dispose();
                    _observers.OnCompleted<TDestination>();
                }
                catch (Exception ex)
                {
                    System.Diagnostics.Trace.TraceError(ex.ToString());
                }
                finally
                {
                    Status = StatusCode.Completed;
                }
                
            }
        }
    }
}

